本文主要會談到 web worker、SIMD 與 asm.js。
到目前為止我們只談論了如何有效的運用非同步處理模式,現在就來探討為什麼非同步處理對 JavaScript 來說這麼重要,其中最明顯的理由就是效能,本文主要探討的是更整體的程式層級的效能。
Web Worker 是屬於瀏覽器的新功能,而非 JavaScript 本身提供的功能,我們可以想像為 JavaScript 程式碼會在主執行緒(main thread)執行,並且又可以另闢一個戰場 worker thread 來提供 JavaScript 在背景執行,必要時再透過訊息溝通,這樣的好處是不阻塞 main thread 而可讓速度更快,比起目前的非同步的共時,效能更佳。
圖片來源:Browser Rendering Optimization - JavaScript
從範例理解 web worker 到底是什麼和怎麼用吧 (๑•̀ㅂ•́)و✧
...
...
由於 worker 是需要起 server 的,推薦使用 Web Server for Chrome 來執行範例網站。
...
...
這裡簡單使用 dedicated web worker 來做相乘運算,算完後再丟回 main thread,點這裡看完整程式碼,點那裡看 Demo。
範例如下。
if (window.Worker) {
var myWorker = new Worker('worker.js');
first.onchange = function() {
myWorker.postMessage([first.value,second.value]);
console.log('Message posted to worker');
};
second.onchange = function() {
myWorker.postMessage([first.value,second.value]);
console.log('Message posted to worker');
};
myWorker.onmessage = function(e) {
result.textContent = e.data;
console.log('Message received from worker');
};
}
說明
worker.js
),讓這個檔案在這個執行緒中單獨執行,我們稱這個獨立運作的 worker 為 dedicated worker。注意
importScripts()
來引用相同網域的程式碼腳本與函式庫。dedicated worker 使用的是獨立單一的 worker,若執行相同的任務還開獨立的 worker 就太浪費了,改用 shared worker 吧,這樣就可以共享資源了。
shared web worker 能夠被多個程式腳本存取,即使是跨越不同 window、iframe 或 worker。這個範例是使用同一個 worker 來做兩數相乘的運算,但可被不同的程式取用,分別達到兩數相乘(multiply.js)與計算平方(square.js)的功能,點這裡看完整程式碼,點那裡看 Demo。
兩數相乘。
計算平方。
shared worker 和 dedicated worker 作法類似,除了以下三個部份是不一樣的...
用 SharedWorker 建構子來產生 shared worker。
腳本可共用 shared worker 來做運算,例如,square.js 和 multiply.js 共用 worker.js 來做兩數相乘的運算;而 dedicated worker 是個別程式單獨使用一個 worker。
與 shared worker 的溝通必須要透過 port 物件,其實 dedicated workers 也是如此,只不過一切是在背景自動完成。
以下是主程式的部分程式碼,用來將要做運算的數字丟給 worker 來計算。
squareNumber.onchange = function() {
myWorker.port.postMessage([squareNumber.value, squareNumber.value]);
console.log('Message posted to worker');
}
以下是 web workers 的部分程式碼。
onconnect = function(e) { // 監聽連線建立的 onconnect 事件,從 onconnect 取得 port 物件
var port = e.ports[0];
port.onmessage = function(e) {
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
port.postMessage(workerResult);
}
}
主流瀏覽器皆支援。
...
...
這麼好用的東西怎麼可以不支援!
...
...
注意,shared web workers 目前只能用於 Chrome 66+、Firefox 60+,且手機幾乎不支援。
圖片來源:Can I use...Shared Web Workers
...
...
手機幾乎不支援???真是太糟糕惹
...
...
本書作者推薦自己撰寫的 polyfill,他覺得其他人寫得都不夠好。
SIMD 是指「單指令流多資料流」(single instruction, multiple data),這是資料平行運算的一種形式,相較於 web worker 是針對程式邏輯區塊的平行運算而言,SIMD 更強調的是資料的位元能被平行處理。因此,SIMD 用的不是瀏覽器所提供的多執行緒的方式,而是 CPU 所提供的指令-短的向量型別與 API。
更多資訊可參考這裡。
asm.js 是用來指稱 JavaScript 中可高度最佳化的一個子集,藉由避開難以最佳化的部份,像是垃圾回收、強制轉型等,asm.js 的程式碼可被 JavaScript 引擎識別出來並特別關注,積極的進行最佳化。
...
...
那麼,該怎麼使用 asm.js 來進行最佳化呢?
asm.js 與型別和強制轉型關係密切,這是因為 JavaScript 引擎花費了大量的功夫在追蹤變數與其所轉換的多種型別的值,這樣才能在必要時刻處理型別間的強制轉型。
因此,我們可以透過一些技巧來提示 JavaScript 引擎某些變數或作業預期得到什麼型別?讓它能跳過這些強制轉型的追蹤步驟。
例如...
var a = 42;
var b = a; // 這裡可能會進行型別轉換...
我們可利用 |
(二進位的 OR)與 0 來確保 b 會被當成一個 32 位元的整數,而不需要追蹤強制轉型的步驟。
var a = 42;
var b = a | 0;
這樣能確保這絕對是 32 位元的整數加法。
(a + b) | 0;
除了型別與強制轉型外,另一個 JavaScript 引擎大量耗損效能的地方是在記憶體配置、垃圾處理與範疇的存取,對此 asm.js 來說,解法就是宣告一個 asm.js 模組,其中要傳入一個 stdlib 的命名空間來匯入所需符號,再來宣告一個 heap 作為記憶體的保留區域,這樣之後就不用再多要求記憶體或釋放已使用過的記憶體(就是一個先預支固定額度以免之後被打擾的概念)。
範例如下,定義一個函式 foo,它接受起始與結束的整數 x 和 y,並計算這範圍中兩兩相鄰的數值的乘積,最後將乘積相加再取平均。
function fooASM(stdlib, foreign, heap) {
'use asm';
var arr = new stdlib.Int32Array(heap);
function foo(x, y) {
x = x | 0;
y = y | 0;
var i = 0;
var p = 0;
var sum = 0;
var count = ((y|0) - (x|0)) | 0;
// 計算兩兩相鄰的數值的乘積
for (i = x | 0; (i | 0) < (y | 0); p = (p + 8) | 0, i = (i + 1) | 0) {
// 儲存結果
arr[ p >> 3 ] = (i * (i + 1)) | 0;
}
// 將乘積相加再取平均
for (i = 0, p = 0; (i | 0) < (count | 0); p = (p + 8) | 0, i = (i + 1) | 0) {
sum = (sum + arr[ p >> 3 ]) | 0;
}
return +(sum / count);
}
return {
foo: foo
};
}
var heap = new ArrayBuffer(0x1000);
var foo = fooASM(window, null, heap).foo;
foo( 10, 20 ); // 233
...
...
嚇到了吧!
...
...
由以上程式碼可知,這種撰寫方式雖然效能佳,但並不好閱讀與廣泛使用,它只能用於針對特殊任務做最佳化處理,像是大量的數學運算、遊戲中的圖形處理等。
看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到...
同步發表於部落格。